// $Id$

package net.sf.persist;

import java.io.IOException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * The main class for the persistence engine.
 * <p>
 * A Persist instance is bound to a {@link java.sql.Connection} object.
 * Internally, Persist caches table-object mappings under <i>cache names</i>
 * that allow for different mappings (most likely from different database
 * schemas) to coexist. The default cache name is used if no cache name is
 * specified in the constructor.
 * <p>
 * Persist instances are not thread safe, in particular because
 * {@link java.sql.Connection} objects are not thread safe.
 * <p>
 * Persist instances are created with the following defaults:
 * <ul>
 * <li> closePreparedStatementsAfterRead=true This will work for most reads
 * (select queries) that do not return long-lasting objects, such as streams or
 * LOB handlers. If a query returns InputStream, Reader, Blob or Clob objects,
 * closePreparedStatementsAfterRead should be set to false, and closing the
 * PreparedStatement must be controlled manually. This is because those
 * datatypes stream data from database after the PreparedStatement execution.
 * <li> updateAutoGeneratedKeys=false This means that objects that are inserted
 * either using insert() or executeUpdate() (with autoGeneratedKeys option) will
 * not have their primary keys automatically updated against the generated keys
 * in the database. Please consult your JDBC driver support for querying
 * auto-generated keys in {@link java.sql.PreparedStatement}
 * <li> {@link DefaultNameGuesser} which will take names in the form
 * CompoundName (for classes) or compoundName (for fields) and return a set of
 * guessed names such as [compound_name, compound_names, compoundname,
 * compoundnames].
 * </ul>
 * 
 * @see TableMapping
 */
public final class Persist {

	private Connection connection;
	private boolean updateAutoGeneratedKeys = false;
	private PreparedStatement lastPreparedStatement = null;
	private boolean closePreparedStatementsAfterRead = true;

	private static ConcurrentMap<String, ConcurrentMap<Class, Mapping>> mappingCaches = new ConcurrentHashMap();
	private static ConcurrentMap<String, NameGuesser> nameGuessers = new ConcurrentHashMap();

	private static final String DEFAULT_CACHE = "default cache";

	private String cacheName = DEFAULT_CACHE;
	private NameGuesser nameGuesser = null;

	static {
		mappingCaches.put(DEFAULT_CACHE, new ConcurrentHashMap());
		nameGuessers.put(DEFAULT_CACHE, new DefaultNameGuesser());

		if (Log.isDebugEnabled(Log.ENGINE)) {
			Log.debug(Log.ENGINE, "Caches initialized");
		}
	}

	// ---------- constructors ----------

	/**
	 * Creates a Persist instance that will use the default cache for
	 * table-object mappings.
	 * 
	 * @param connection {@link java.sql.Connection} object to be used
	 * @since 1.0
	 */
	public Persist(Connection connection) {
		this(DEFAULT_CACHE, connection);
	}

	/**
	 * Creates a Persist instance that will use the given cache name for
	 * table-object mappings.
	 * 
	 * @param cacheName Name of the cache to be used
	 * @param connection {@link java.sql.Connection} object to be used
	 * @since 1.0
	 */
	public Persist(String cacheName, Connection connection) {

		if (cacheName == null) {
			cacheName = DEFAULT_CACHE;
		}

		this.cacheName = cacheName;
		this.connection = connection;

		this.nameGuesser = nameGuessers.get(cacheName);
		if (this.nameGuesser == null) {
			// this block may execute more than once from different threads --
			// not a problem, though
			this.nameGuesser = new DefaultNameGuesser();
			nameGuessers.put(cacheName, this.nameGuesser);
		}

		if (Log.isDebugEnabled(Log.ENGINE)) {
			Log.debug(Log.ENGINE, "New instance for cache [" + cacheName + "] and connection [" + connection + "]");
		}
	}

	// ---------- name guesser ----------

	/**
	 * Sets the {@link NameGuesser} for a given mappings cache.
	 * 
	 * @param cacheName Name of the cache to be used
	 * @param nameGuesser {@link NameGuesser} implementation
	 * @since 1.0
	 */
	public static void setNameGuesser(final String cacheName, final NameGuesser nameGuesser) {
		nameGuessers.put(cacheName, nameGuesser);

		// purge mappings cache so that name mappings are coherent
		mappingCaches.put(cacheName, new ConcurrentHashMap());

		if (Log.isDebugEnabled(Log.ENGINE)) {
			Log.debug(Log.ENGINE, "Name guesser set for cache [" + cacheName + "]");
		}
	}

	/**
	 * Sets the name guesser for the default mappings cache.
	 * 
	 * @param nameGuesser {@link NameGuesser} implementation
	 * @since 1.0
	 */
	public static void setNameGuesser(final NameGuesser nameGuesser) {
		nameGuessers.put(DEFAULT_CACHE, nameGuesser);
	}

	// ---------- autoUpdateGeneratedKeys getter/setter ----------

	/**
	 * Sets the behavior for updating auto-generated keys.
	 * 
	 * @param updateAutoGeneratedKeys if set to true, auto-generated keys will
	 * be updated after the execution of insert or executeUpdate operations that
	 * may trigger auto-generation of keys in the database
	 * @since 1.0
	 */
	public void setUpdateAutoGeneratedKeys(final boolean updateAutoGeneratedKeys) {
		this.updateAutoGeneratedKeys = updateAutoGeneratedKeys;

		if (Log.isDebugEnabled(Log.ENGINE)) {
			Log.debug(Log.ENGINE, "setUpdateAutoGeneratedKeys(" + updateAutoGeneratedKeys + ")");
		}
	}

	/**
	 * Returns true if updating auto-generated keys is enabled.
	 */
	public boolean isUpdateAutoGeneratedKeys() {
		return updateAutoGeneratedKeys;
	}

	// ---------- mappings cache ----------

	/**
	 * Returns the mapping for the given object class.
	 * 
	 * @param objectClass {@link java.lang.Class} object to get a
	 * {@link TableMapping} for
	 * @since 1.0
	 */
	public Mapping getMapping(final Class objectClass) {

		if (cacheName == null) {
			cacheName = DEFAULT_CACHE;
		}

		if (!mappingCaches.containsKey(cacheName)) {
			// more than one map may end up being inserted here for the same
			// cacheName, but this is not problematic
			mappingCaches.put(cacheName, new ConcurrentHashMap());
		}

		final ConcurrentMap<Class, Mapping> mappingCache = mappingCaches.get(cacheName);

		if (!mappingCache.containsKey(objectClass)) {
			try {
				// more than one map may end up being inserted here for the same
				// objectClass, but this is not
				// problematic
				mappingCache.put(objectClass, Mapping.getMapping(connection.getMetaData(), objectClass, nameGuesser));

				if (Log.isDebugEnabled(Log.ENGINE)) {
					Log.debug(Log.ENGINE, "Cached mapping for [" + objectClass.getCanonicalName() + "]");
				}
			} catch (SQLException e) {
				throw new RuntimeSQLException(e);
			}
		}

		return mappingCache.get(objectClass);
	}

	/**
	 * Utility method that will get a TableMapping for a given class. If the
	 * mapping for the class is not a TableMapping, will throw an exception
	 * specifying the given calling method name.
	 */
	private TableMapping getTableMapping(Class objectClass, String callingMethodName) {
		final Mapping mapping = getMapping(objectClass);
		if (!(mapping instanceof TableMapping)) {
			throw new PersistException("Class [" + objectClass.getCanonicalName()
					+ "] has a @NoTable annotation defined, therefore " + callingMethodName + " can't work with it. "
					+ "If this class is supposed to be mapped to a table, @NoTable should not be used.");
		}
		return (TableMapping) mapping;
	}

	// ---------- connection ----------

	/**
	 * Returns the {@link java.sql.Connection Connection} associated with this
	 * Persist instance.
	 * @since 1.0
	 */
	public Connection getConnection() {
		return connection;
	}

	/**
	 * Commits the {@link java.sql.Connection Connection} associated with this
	 * Persist instance.
	 * 
	 * @see java.sql.Connection#commit()
	 * @since 1.0
	 */
	public void commit() {
		try {
			connection.commit();

			if (Log.isDebugEnabled(Log.ENGINE)) {
				Log.debug(Log.ENGINE, "Connection commited");
			}
		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		}
	}

	/**
	 * Rolls back the {@link java.sql.Connection Connection} associated with
	 * this Persist instance.
	 * 
	 * @see java.sql.Connection#rollback()
	 * @since 1.0
	 */
	public void rollback() {
		try {
			connection.rollback();

			if (Log.isDebugEnabled(Log.ENGINE)) {
				Log.debug(Log.ENGINE, "Connection rolled back");
			}
		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		}
	}

	/**
	 * Sets the auto commit behavior for the
	 * {@link java.sql.Connection Connection} associated with this Persist
	 * instance.
	 * @see java.sql.Connection#setAutoCommit(boolean)
	 * @since 1.0
	 */
	public void setAutoCommit(final boolean autoCommit) {
		try {
			connection.setAutoCommit(autoCommit);

			if (Log.isDebugEnabled(Log.ENGINE)) {
				Log.debug(Log.ENGINE, "Connection setAutoCommit(" + autoCommit + ")");
			}
		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		}
	}

	// ---------- prepared statement ----------

	/**
	 * Creates a {@link java.sql.PreparedStatement}, setting the names of the
	 * auto-generated keys to be retrieved.
	 * 
	 * @param sql SQL statement to create the {@link java.sql.PreparedStatement}
	 * from
	 * @param autoGeneratedKeys names of the columns that will have
	 * auto-generated values produced during the execution of the
	 * {@link java.sql.PreparedStatement}
	 * @since 1.0
	 */
	public PreparedStatement getPreparedStatement(final String sql, final String[] autoGeneratedKeys) {
		try {
			if (autoGeneratedKeys == null || autoGeneratedKeys.length == 0) {
				lastPreparedStatement = getPreparedStatement(sql);
			} else {
				lastPreparedStatement = connection.prepareStatement(sql, autoGeneratedKeys);
			}
		} catch (SQLException e) {
			throw new RuntimeSQLException("Error creating prepared statement for sql [" + sql
					+ "] with autoGeneratedKeys " + Arrays.toString(autoGeneratedKeys) + ": " + e.getMessage(), e);
		}

		if (Log.isDebugEnabled(Log.ENGINE)) {
			Log.debug(Log.ENGINE, "Generated PreparedStatement [" + lastPreparedStatement + "] for [" + sql
					+ "] using autoGeneratedKeys " + Arrays.toString(autoGeneratedKeys));
		}

		return lastPreparedStatement;
	}

	/**
	 * Creates a {@link java.sql.PreparedStatement} with no parameters.
	 * 
	 * @param sql SQL statement to create the {@link java.sql.PreparedStatement}
	 * from
	 * @since 1.0
	 */
	public PreparedStatement getPreparedStatement(final String sql) {

		try {
			lastPreparedStatement = connection.prepareStatement(sql);
		} catch (SQLException e) {
			throw new RuntimeSQLException("Error creating prepared statement for sql [" + sql + "]: " + e.getMessage(),
					e);
		}

		if (Log.isDebugEnabled(Log.ENGINE)) {
			Log.debug(Log.ENGINE, "Generated PreparedStatement [" + lastPreparedStatement + "] for [" + sql + "]");
		}

		return lastPreparedStatement;
	}

	/**
	 * Closes a {@link java.sql.PreparedStatement}.
	 * 
	 * @param statement {@link java.sql.PreparedStatement} to be closed
	 * @see java.sql.PreparedStatement#close()
	 * @since 1.0
	 */
	public void closePreparedStatement(final PreparedStatement statement) {
		try {
			if (statement != null) {
				statement.close();
			}
		} catch (SQLException e) {
			throw new RuntimeSQLException("Error closing prepared statement: " + e.getMessage(), e);
		}

		if (Log.isDebugEnabled(Log.ENGINE)) {
			Log.debug(Log.ENGINE, "Closed PreparedStatement [" + statement + "]");
		}
	}

	/**
	 * Returns the last {@link java.sql.PreparedStatement} used by the engine.
	 * 
	 * @since 1.0
	 */
	public PreparedStatement getLastPreparedStatement() {
		return lastPreparedStatement;
	}

	/**
	 * Closes the last {@link java.sql.PreparedStatement} used by the engine.
	 * 
	 * @see java.sql.PreparedStatement#close()
	 * @since 1.0
	 */
	public void closeLastPreparedStatement() {
		closePreparedStatement(lastPreparedStatement);
		lastPreparedStatement = null;
	}

	/**
	 * Sets the behavior for closing {@link java.sql.PreparedStatement}
	 * instances after execution. This will only affect reads, since any update
	 * operations (insert, delete, update) will always have their
	 * {@link java.sql.PreparedStatement} instances automatically closed.
	 * <p>
	 * If a query returns InputStream, Reader, Blob or Clob objects, this should
	 * be set to false, and closing the PreparedStatement must be controlled
	 * manually. This is because those datatypes stream data from database after
	 * the PreparedStatement execution.
	 * 
	 * @param closePreparedStatementsAfterRead if true,
	 * {@link java.sql.PreparedStatement} instances for read queries will be
	 * automatically closed
	 * @since 1.0
	 */
	public void setClosePreparedStatementsAfterRead(final boolean closePreparedStatementsAfterRead) {
		this.closePreparedStatementsAfterRead = closePreparedStatementsAfterRead;

		if (Log.isDebugEnabled(Log.ENGINE)) {
			Log.debug(Log.ENGINE, "setClosePreparedStatementsAfterRead(" + closePreparedStatementsAfterRead + ")");
		}
	}

	/**
	 * Returns true if {@link java.sql.PreparedStatement} instances are
	 * automatically closed after read (select or otherwise) queries.
	 * 
	 * @since 1.0
	 */
	public boolean isClosePreparedStatementsAfterRead() {
		return this.closePreparedStatementsAfterRead;
	}

	// ---------- mappers ----------

	/**
	 * Sets parameters in the given prepared statement.
	 * <p>
	 * Parameters will be set using PreparedStatement set methods related with
	 * the Java types of the parameters, according with the following table:
	 * <ul>
	 * <li> Boolean/boolean: setBoolean
	 * <li> Byte/byte: setByte
	 * <li> Short/short: setShort
	 * <li> Integer/integer: setInt
	 * <li> Long/long: setLong
	 * <li> Float/float: setFloat
	 * <li> Double/double: setDouble
	 * <li> Character/char: setString
	 * <li> Character[]/char[]: setString
	 * <li> Byte[]/byte[]: setBytes
	 * <li> String: setString
	 * <li> java.math.BigDecimal: setBigDecimal
	 * <li> java.io.Reader: setCharacterStream
	 * <li> java.io.InputStream: setBinaryStream
	 * <li> java.util.Date: setTimestamp
	 * <li> java.sql.Date: setDate
	 * <li> java.sql.Time: setTime
	 * <li> java.sql.Timestamp: setTimestamp
	 * <li> java.sql.Clob : setClob
	 * <li> java.sql.Blob: setBlob
	 * </ul>
	 * 
	 * @param stmt {@link java.sql.PreparedStatement} to have parameters set
	 * into
	 * @param parameters varargs or Object[] with parameters values
	 * @throws RuntimeSQLException if a database access error occurs or this
	 * method is called on a closed PreparedStatement; if a parameter type does
	 * not have a matching set method (as outlined above)
	 * @throws RuntimeIOException if an error occurs while reading data from a
	 * Reader or InputStream parameter
	 * @since 1.0
	 */
	public static void setParameters(final PreparedStatement stmt, final Object[] parameters) {

		// if no parameters, do nothing
		if (parameters == null || parameters.length == 0) {
			return;
		}

		ParameterMetaData stmtMetaData = null;

		for (int i = 1; i <= parameters.length; i++) {

			final Object parameter = parameters[i - 1];

			if (parameter == null) {

				// lazy assignment of stmtMetaData
				if (stmtMetaData == null) {
					try {
						stmtMetaData = stmt.getParameterMetaData();
					} catch (SQLException e) {
						throw new RuntimeSQLException(e);
					}
				}

				// get sql type from prepared statement metadata
				int sqlType;
				try {
					sqlType = stmtMetaData.getParameterType(i);
				} catch (SQLException e2) {
					// feature not supported, use NULL
					sqlType = java.sql.Types.NULL;
				}

				try {
					stmt.setNull(i, sqlType);
				} catch (SQLException e) {
					throw new RuntimeSQLException("Could not set null into parameter [" + i
							+ "] using java.sql.Types [" + Log.sqlTypeToString(sqlType) + "] " + e.getMessage(), e);
				}

				if (Log.isDebugEnabled(Log.PARAMETERS)) {
					Log.debug(Log.PARAMETERS, "Parameter [" + i + "] from PreparedStatement [" + stmt
							+ "] set to [null] using java.sql.Types [" + Log.sqlTypeToString(sqlType) + "]");
				}

				continue;
			}

			try {

				final Class type = parameter.getClass();

				if (type == Boolean.class || type == boolean.class) {
					stmt.setBoolean(i, (Boolean) parameter);
				} else if (type == Byte.class || type == byte.class) {
					stmt.setByte(i, (Byte) parameter);
				} else if (type == Short.class || type == short.class) {
					stmt.setShort(i, (Short) parameter);
				} else if (type == Integer.class || type == int.class) {
					stmt.setInt(i, (Integer) parameter);
				} else if (type == Long.class || type == long.class) {
					stmt.setLong(i, (Long) parameter);
				} else if (type == Float.class || type == float.class) {
					stmt.setFloat(i, (Float) parameter);
				} else if (type == Double.class || type == double.class) {
					stmt.setDouble(i, (Double) parameter);
				} else if (type == Character.class || type == char.class) {
					stmt.setString(i, parameter == null ? null : "" + (Character) parameter);
				} else if (type == char[].class) {
					// not efficient, will create a new String object
					stmt.setString(i, parameter == null ? null : new String((char[]) parameter));
				} else if (type == Character[].class) {
					// not efficient, will duplicate the array and create a new String object
					final Character[] src = (Character[]) parameter;
					final char[] dst = new char[src.length];
					for (int j = 0; j < src.length; j++) { // can't use System.arraycopy here
						dst[j] = src[j];
					}
					stmt.setString(i, new String(dst));
				} else if (type == String.class) {
					stmt.setString(i, (String) parameter);
				} else if (type == BigDecimal.class) {
					stmt.setBigDecimal(i, (BigDecimal) parameter);
				} else if (type == byte[].class) {
					stmt.setBytes(i, (byte[]) parameter);
				} else if (type == Byte[].class) {
					// not efficient, will duplicate the array
					final Byte[] src = (Byte[]) parameter;
					final byte[] dst = new byte[src.length];
					for (int j = 0; j < src.length; j++) { // can't use System.arraycopy here
						dst[j] = src[j];
					}
					stmt.setBytes(i, dst);
				} else if (parameter instanceof java.io.Reader) {
					final java.io.Reader reader = (java.io.Reader) parameter;

					// the jdbc api for setCharacterStream requires the number
					// of characters to be read so this will end up reading 
					// data twice (here and inside the jdbc driver)
					// besides, the reader must support reset()
					int size = 0;
					try {
						reader.reset();
						while (reader.read() != -1) {
							size++;
						}
						reader.reset();
					} catch (IOException e) {
						throw new RuntimeIOException(e);
					}
					stmt.setCharacterStream(i, reader, size);
				} else if (parameter instanceof java.io.InputStream) {
					final java.io.InputStream inputStream = (java.io.InputStream) parameter;

					// the jdbc api for setBinaryStream requires the number of
					// bytes to be read so this will end up reading the stream 
					// twice (here and inside the jdbc driver)
					// besides, the stream must support reset()
					int size = 0;
					try {
						inputStream.reset();
						while (inputStream.read() != -1) {
							size++;
						}
						inputStream.reset();
					} catch (IOException e) {
						throw new RuntimeIOException(e);
					}
					stmt.setBinaryStream(i, inputStream, size);
				} else if (parameter instanceof Clob) {
					stmt.setClob(i, (Clob) parameter);
				} else if (parameter instanceof Blob) {
					stmt.setBlob(i, (Blob) parameter);
				} else if (type == java.util.Date.class) {
					final java.util.Date date = (java.util.Date) parameter;
					stmt.setTimestamp(i, new java.sql.Timestamp(date.getTime()));
				} else if (type == java.sql.Date.class) {
					stmt.setDate(i, (java.sql.Date) parameter);
				} else if (type == java.sql.Time.class) {
					stmt.setTime(i, (java.sql.Time) parameter);
				} else if (type == java.sql.Timestamp.class) {
					stmt.setTimestamp(i, (java.sql.Timestamp) parameter);
				} else {
					// last resort; this should cover all database-specific
					// object types
					stmt.setObject(i, parameter);
				}

				if (Log.isDebugEnabled(Log.PARAMETERS)) {
					Log.debug(Log.PARAMETERS, "PreparedStatement [" + stmt + "] Parameter [" + i + "] type ["
							+ type.getSimpleName() + "] set to [" + Log.objectToString(parameter) + "]");
				}

			} catch (SQLException e) {
				throw new RuntimeSQLException(e);
			}
		}
	}

	/**
	 * Returns true if the provided class is a type supported natively (as
	 * opposed to a bean).
	 * 
	 * @param type {@link java.lang.Class} type to be tested
	 * @since 1.0
	 */
	private static boolean isNativeType(final Class type) {

		// to return an arbitrary object use Object.class

		return (type == boolean.class || type == Boolean.class || type == byte.class || type == Byte.class
				|| type == short.class || type == Short.class || type == int.class || type == Integer.class
				|| type == long.class || type == Long.class || type == float.class || type == Float.class
				|| type == double.class || type == Double.class || type == char.class || type == Character.class
				|| type == byte[].class || type == Byte[].class || type == char[].class || type == Character[].class
				|| type == String.class || type == BigDecimal.class || type == java.util.Date.class
				|| type == java.sql.Date.class || type == java.sql.Time.class || type == java.sql.Timestamp.class
				|| type == java.io.InputStream.class || type == java.io.Reader.class || type == java.sql.Clob.class
				|| type == java.sql.Blob.class || type == Object.class);
	}

	/**
	 * Reads a column from the current row in the provided
	 * {@link java.sql.ResultSet} and returns an instance of the specified Java
	 * {@link java.lang.Class} containing the values read.
	 * <p>
	 * This method is used while converting {@link java.sql.ResultSet} rows to
	 * objects. The class type is the field type in the target bean.
	 * <p>
	 * Correspondence between class types and ResultSet.get methods is as
	 * follows:
	 * <ul>
	 * <li> Boolean/boolean: getBoolean
	 * <li> Byte/byte: getByte
	 * <li> Short/short: getShort
	 * <li> Integer/int: getInt
	 * <li> Long/long: getLong
	 * <li> Float/float: getFloat
	 * <li> Double/double: getDouble
	 * <li> Character/char: getString
	 * <li> Character[]/char[]: getString
	 * <li> Byte[]/byte[]: setBytes
	 * <li> String: setString
	 * <li> java.math.BigDecimal: getBigDecimal
	 * <li> java.io.Reader: getCharacterStream
	 * <li> java.io.InputStream: getBinaryStream
	 * <li> java.util.Date: getTimestamp
	 * <li> java.sql.Date: getDate
	 * <li> java.sql.Time: getTime
	 * <li> java.sql.Timestamp: getTimestamp
	 * <li> java.sql.Clob: getClob
	 * <li> java.sql.Blob: getBlob
	 * </ul>
	 * <p>
	 * null's will be respected for any non-native types. This means that if a
	 * field is of type Integer it will be able to receive a null value from the
	 * ResultSet; on the other hand, if a field is of type int it will receive 0
	 * for a null value from the {@link java.sql.ResultSet}.
	 * 
	 * @param resultSet {@link java.sql.ResultSet} (positioned in the row to be
	 * processed)
	 * @param column column index in the result set (starting with 1)
	 * @param type {@link java.lang.Class} of the object to be returned
	 * @since 1.0
	 */
	public static Object getValueFromResultSet(final ResultSet resultSet, final int column, final Class type) {

		Object value = null;

		try {

			if (type == boolean.class) {
				value = resultSet.getBoolean(column);
			} else if (type == Boolean.class) {
				value = resultSet.getObject(column) == null ? null : resultSet.getBoolean(column);
			} else if (type == byte.class) {
				value = resultSet.getByte(column);
			} else if (type == Byte.class) {
				value = resultSet.getObject(column) == null ? null : resultSet.getByte(column);
			} else if (type == short.class) {
				value = resultSet.getShort(column);
			} else if (type == Short.class) {
				value = resultSet.getObject(column) == null ? null : resultSet.getShort(column);
			} else if (type == int.class) {
				value = resultSet.getInt(column);
			} else if (type == Integer.class) {
				value = resultSet.getObject(column) == null ? null : resultSet.getInt(column);
			} else if (type == long.class) {
				value = resultSet.getLong(column);
			} else if (type == Long.class) {
				value = resultSet.getObject(column) == null ? null : resultSet.getLong(column);
			} else if (type == float.class) {
				value = resultSet.getFloat(column);
			} else if (type == Float.class) {
				value = resultSet.getObject(column) == null ? null : resultSet.getFloat(column);
			} else if (type == double.class) {
				value = resultSet.getDouble(column);
			} else if (type == Double.class) {
				value = resultSet.getObject(column) == null ? null : resultSet.getDouble(column);
			} else if (type == BigDecimal.class) {
				value = resultSet.getObject(column) == null ? null : resultSet.getBigDecimal(column);
			} else if (type == String.class) {
				value = resultSet.getString(column);
			} else if (type == Character.class || type == char.class) {
				final String str = resultSet.getString(column);
				if (str != null && str.length() > 1) {
					throw new PersistException("Column [" + column + "] returned a string with length ["
							+ str.length() + "] but field type [" + type.getSimpleName()
							+ "] can only accept 1 character");
				}
				value = (str == null || str.length() == 0) ? null : str.charAt(0);
			} else if (type == byte[].class || type == Byte[].class) {
				value = resultSet.getBytes(column);
			} else if (type == char[].class || type == Character[].class) {
				final String str = resultSet.getString(column);
				value = (str == null) ? null : str.toCharArray();
			} else if (type == java.util.Date.class) {
				final java.sql.Timestamp timestamp = resultSet.getTimestamp(column);
				value = (timestamp == null) ? null : new java.util.Date(timestamp.getTime());
			} else if (type == java.sql.Date.class) {
				value = resultSet.getDate(column);
			} else if (type == java.sql.Time.class) {
				value = resultSet.getTime(column);
			} else if (type == java.sql.Timestamp.class) {
				value = resultSet.getTimestamp(column);
			} else if (type == java.io.InputStream.class) {
				value = resultSet.getBinaryStream(column);
			} else if (type == java.io.Reader.class) {
				value = resultSet.getCharacterStream(column);
			} else if (type == java.sql.Clob.class) {
				value = resultSet.getClob(column);
			} else if (type == java.sql.Blob.class) {
				value = resultSet.getBlob(column);
			} else {
				// this will work for database-specific types
				value = resultSet.getObject(column);
			}

		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		}

		if (Log.isDebugEnabled(Log.RESULTS)) {
			Log.debug(Log.RESULTS, "Read ResultSet [" + resultSet + "] column [" + column + "]"
					+ (value == null ? "" : " type [" + value.getClass().getSimpleName() + "]") + " value ["
					+ Log.objectToString(value) + "]");
		}

		return value;
	}

	/**
	 * Reads a column from the current row in the provided
	 * {@link java.sql.ResultSet} and return a value correspondent to the SQL
	 * type provided (as defined in {@link java.sql.Types java.sql.Types}).
	 * <p>
	 * This method is used while converting results sets to maps. The SQL type
	 * comes from the {@link java.sql.ResultSetMetaData ResultSetMetaData} for a
	 * given column.
	 * <p>
	 * Correspondence between {@link java.sql.Types java.sql.Types} and
	 * ResultSet.get methods is as follows:
	 * <ul>
	 * <li> ARRAY: getArray
	 * <li> BIGINT: getLong
	 * <li> BIT: getBoolean
	 * <li> BLOB: getBytes
	 * <li> BOOLEAN: getBoolean
	 * <li> CHAR: getString
	 * <li> CLOB: getString
	 * <li> DATALINK: getBinaryStream
	 * <li> DATE: getDate
	 * <li> DECIMAL: getBigDecimal
	 * <li> DOUBLE: getDouble
	 * <li> FLOAT: getFloat
	 * <li> INTEGER: getInt
	 * <li> JAVA_OBJECT: getObject
	 * <li> LONGVARBINARY: getBytes
	 * <li> LONGVARCHAR: getString
	 * <li> NULL: getNull
	 * <li> NCHAR: getString
	 * <li> NUMERIC: getBigDecimal
	 * <li> OTHER: getObject
	 * <li> REAL: getDouble
	 * <li> REF: getRef
	 * <li> SMALLINT: getInt
	 * <li> TIME: getTime
	 * <li> TIMESTAMP: getTimestamp
	 * <li> TINYINT: getInt
	 * <li> VARBINARY: getBytes
	 * <li> VARCHAR: getString
	 * <li> [Oracle specific] 100: getFloat
	 * <li> [Oracle specific] 101: getDouble
	 * </ul>
	 * <p>
	 * null's are respected for all types. This means that if a column is of
	 * type LONG and its value comes from the database as null, this method will
	 * return null for it.
	 * 
	 * @param resultSet {@link java.sql.ResultSet} (positioned in the row to be
	 * processed)
	 * @param column Column index in the result set (starting with 1)
	 * @param type type of the column (as defined in
	 * {@link java.sql.Types java.sql.Types})
	 * @since 1.0
	 */
	public static Object getValueFromResultSet(final ResultSet resultSet, final int column, final int type) {

		Object value = null;

		try {

			if (type == java.sql.Types.ARRAY) {
				value = resultSet.getArray(column);
			} else if (type == java.sql.Types.BIGINT) {
				value = resultSet.getObject(column) == null ? null : resultSet.getLong(column);
			} else if (type == java.sql.Types.BINARY) {
				value = resultSet.getBytes(column);
			} else if (type == java.sql.Types.BIT) {
				value = resultSet.getObject(column) == null ? null : resultSet.getBoolean(column);
			} else if (type == java.sql.Types.BLOB) {
				value = resultSet.getBytes(column);
			} else if (type == java.sql.Types.BOOLEAN) {
				value = resultSet.getObject(column) == null ? null : resultSet.getBoolean(column);
			} else if (type == java.sql.Types.CHAR) {
				value = resultSet.getString(column);
			} else if (type == java.sql.Types.CLOB) {
				value = resultSet.getString(column);
			} else if (type == java.sql.Types.DATALINK) {
				value = resultSet.getBinaryStream(column);
			} else if (type == java.sql.Types.DATE) {
				value = resultSet.getDate(column);
			} else if (type == java.sql.Types.DECIMAL) {
				value = resultSet.getBigDecimal(column);
			} else if (type == java.sql.Types.DOUBLE) {
				value = resultSet.getObject(column) == null ? null : resultSet.getDouble(column);
			} else if (type == java.sql.Types.FLOAT) {
				value = resultSet.getObject(column) == null ? null : resultSet.getFloat(column);
			} else if (type == java.sql.Types.INTEGER) {
				value = resultSet.getObject(column) == null ? null : resultSet.getInt(column);
			} else if (type == java.sql.Types.JAVA_OBJECT) {
				value = resultSet.getObject(column);
			} else if (type == java.sql.Types.LONGVARBINARY) {
				value = resultSet.getBytes(column);
			} else if (type == java.sql.Types.LONGVARCHAR) {
				value = resultSet.getString(column);
			} else if (type == java.sql.Types.NULL) {
				value = null;
			} else if (type == java.sql.Types.NUMERIC) {
				value = resultSet.getBigDecimal(column);
			} else if (type == java.sql.Types.OTHER) {
				value = resultSet.getObject(column);
			} else if (type == java.sql.Types.REAL) {
				value = resultSet.getObject(column) == null ? null : resultSet.getDouble(column);
			} else if (type == java.sql.Types.REF) {
				value = resultSet.getRef(column);
			} else if (type == java.sql.Types.SMALLINT) {
				value = resultSet.getObject(column) == null ? null : resultSet.getInt(column);
			} else if (type == java.sql.Types.TIME) {
				value = resultSet.getTime(column);
			} else if (type == java.sql.Types.TIMESTAMP) {
				value = resultSet.getTimestamp(column);
			} else if (type == java.sql.Types.TINYINT) {
				value = resultSet.getObject(column) == null ? null : resultSet.getInt(column);
			} else if (type == java.sql.Types.VARBINARY) {
				value = resultSet.getBytes(column);
			} else if (type == java.sql.Types.VARCHAR) {
				value = resultSet.getString(column);
			}

			// oracle specific
			else if (type == 100) {
				value = resultSet.getObject(column) == null ? null : resultSet.getFloat(column);
			} else if (type == 101) {
				value = resultSet.getObject(column) == null ? null : resultSet.getDouble(column);
			}

			else {
				throw new PersistException("Could not get value for result set using type ["
						+ Log.sqlTypeToString(type) + "] on column [" + column + "]");
			}

		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		}

		if (Log.isDebugEnabled(Log.RESULTS)) {
			Log.debug(Log.RESULTS, "Read ResultSet [" + resultSet + "] column [" + column + "] sql type ["
					+ Log.sqlTypeToString(type) + "]"
					+ (value == null ? "" : " type [" + value.getClass().getSimpleName() + "]") + " value ["
					+ Log.objectToString(value) + "]");
		}

		return value;
	}

	/**
	 * Returns a list of values for the fields in the provided object that match
	 * the provided list of columns according with the object mapping.
	 * 
	 * @param object source object to obtain parameter values
	 * @param columns name of the database columns to get parameters for
	 * @param mapping mapping for the object class
	 * @since 1.0
	 */
	private static Object[] getParametersFromObject(final Object object, final String[] columns,
			final TableMapping mapping) {

		Object[] parameters = new Object[columns.length];
		for (int i = 0; i < columns.length; i++) {
			final String columnName = columns[i];
			final Method getter = mapping.getGetterForColumn(columnName);

			Object value = null;
			try {
				value = getter.invoke(object, new Object[] {});
			} catch (Exception e) {
				throw new PersistException("Could not access getter for column [" + columnName + "]", e);
			}

			parameters[i] = value;
		}

		return parameters;
	}

	/**
	 * Reads a row from the provided {@link java.sql.ResultSet} and converts it
	 * to an object instance of the given class.
	 * <p>
	 * See {@link #getValueFromResultSet(ResultSet, int, Class)} for details on
	 * the mappings between ResultSet.get methods and Java types.
	 * 
	 * @param objectClass type of the object to be returned
	 * @param resultSet {@link java.sql.ResultSet} (positioned in the row to be
	 * processed)
	 * @see #isNativeType(Class)
	 * @see #getValueFromResultSet(ResultSet, int, Class)
	 * @since 1.0
	 */
	public Object loadObject(final Class objectClass, final ResultSet resultSet) throws SQLException {

		final ResultSetMetaData resultSetMetaData = resultSet.getMetaData();

		Object ret = null;

		// for native objects (int, long, String, Date, etc.)
		if (isNativeType(objectClass)) {
			if (resultSetMetaData.getColumnCount() != 1) {
				throw new PersistException("ResultSet returned [" + resultSetMetaData.getColumnCount()
						+ "] columns but 1 column was expected to load data into an instance of ["
						+ objectClass.getName() + "]");
			}
			ret = getValueFromResultSet(resultSet, 1, objectClass);
		}

		// for beans
		else {

			final Mapping mapping = getMapping(objectClass);

			try {
				ret = objectClass.newInstance();
			} catch (Exception e) {
				throw new PersistException(e);
			}

			for (int i = 1; i <= resultSetMetaData.getColumnCount(); i++) {
				final String columnName = resultSetMetaData.getColumnName(i).toLowerCase();
				final Method setter = mapping.getSetterForColumn(columnName);
				if (setter == null) {
					throw new PersistException("Column [" + columnName
							+ "] from result set does not have a mapping to a field in [" 
							+ objectClass.getName() + "]");
				}

				final Class type = setter.getParameterTypes()[0];
				final Object value = getValueFromResultSet(resultSet, i, type);

				try {
					setter.invoke(ret, new Object[] { value });
				} catch (Exception e) {
					throw new PersistException("Error setting value [" + value + "]"
							+ (value == null ? "" : " of type [" + value.getClass().getName() + "]") + " from column ["
							+ columnName + "] using setter [" + setter + "]: " + e.getMessage(), e);
				}
			}

		}

		return ret;
	}

	/**
	 * Reads a row from the provided {@link java.sql.ResultSet} and converts it
	 * to a map having the column names as keys and results as values.
	 * <p>
	 * See {@link #getValueFromResultSet(ResultSet, int, int)} for details on
	 * the mappings between ResultSet.get methods and
	 * {@link java.sql.Types java.sql.Types} types
	 * 
	 * @param resultSet {@link java.sql.ResultSet} (positioned in the row to be
	 * processed)
	 * @since 1.0
	 */
	public static Map<String, Object> loadMap(final ResultSet resultSet) throws SQLException {

		final Map ret = new LinkedHashMap();
		final ResultSetMetaData resultSetMetaData = resultSet.getMetaData();

		for (int i = 1; i <= resultSetMetaData.getColumnCount(); i++) {
			final String columnName = resultSetMetaData.getColumnName(i).toLowerCase();
			final int type = resultSetMetaData.getColumnType(i);
			final Object value = getValueFromResultSet(resultSet, i, type);
			ret.put(columnName, value);
		}

		return ret;
	}

	/**
	 * Set auto-generated keys (returned from an insert operation) into an
	 * object.
	 * 
	 * @param object {@link java.lang.Object} to have fields mapped to
	 * auto-generated keys set
	 * @param result {@link Result} containing auto-generated keys
	 * @since 1.0
	 */
	public void setAutoGeneratedKeys(final Object object, final Result result) {

		if (result.getGeneratedKeys().size() != 0) {
			final TableMapping mapping = getTableMapping(object.getClass(), "setAutoGeneratedKeys()");
			for (int i = 0; i < mapping.getAutoGeneratedColumns().length; i++) {
				final String columnName = mapping.getAutoGeneratedColumns()[i];
				final Method setter = mapping.getSetterForColumn(columnName);
				final Object key = result.getGeneratedKeys().get(i);
				try {
					setter.invoke(object, new Object[] { key });
				} catch (Exception e) {
					throw new PersistException("Could not invoke setter [" + setter + "] with auto generated key ["
							+ key + "] of class [" + key.getClass().getName() + "]", e);
				}
			}
		}

	}

	// ---------- execute ----------

	/**
	 * Executes an update and return a {@link Result} object containing the
	 * number of rows modified and auto-generated keys produced.
	 * <p>
	 * Parameters will be set according with the correspondence defined in
	 * {@link #setParameters(PreparedStatement, int[], Object[])}
	 * 
	 * @param objectClass Class of the object related with the query. Used to
	 * determine the types of the auto-incremented keys. Only important if
	 * autoGeneratedKeys contains values.
	 * @param sql SQL code to be executed.
	 * @param autoGeneratedKeys List of columns that are going to be
	 * auto-generated during the query execution.
	 * @param parameters Parameters to be used in the PreparedStatement.
	 * @since 1.0
	 */
	public Result executeUpdate(final Class objectClass, final String sql, final String[] autoGeneratedKeys,
			final Object...parameters) {

		long begin = 0;
		if (Log.isDebugEnabled(Log.PROFILING)) {
			begin = System.currentTimeMillis();
		}

		final PreparedStatement stmt = getPreparedStatement(sql, autoGeneratedKeys);

		try {
			setParameters(stmt, parameters);

			int rowsModified = 0;
			try {
				rowsModified = stmt.executeUpdate();
			} catch (SQLException e) {
				throw new RuntimeSQLException("Error executing sql [" + sql + "] with parameters "
						+ Arrays.toString(parameters) + ": " + e.getMessage(), e);
			}

			final List generatedKeys = new ArrayList();
			if (autoGeneratedKeys.length != 0) {
				try {
					final Mapping mapping = getMapping(objectClass);
					final ResultSet resultSet = stmt.getGeneratedKeys();
					for (int i = 0; i < autoGeneratedKeys.length; i++) {
						resultSet.next();

						// get the auto-generated key using the ResultSet.get method
						// that matches
						// the bean setter parameter type
						final Method setter = mapping.getSetterForColumn(autoGeneratedKeys[i]);
						final Class type = setter.getParameterTypes()[0];
						final Object value = Persist.getValueFromResultSet(resultSet, 1, type);

						generatedKeys.add(value);
					}
					resultSet.close();
				} catch (SQLException e) {
					throw new RuntimeSQLException("This JDBC driver does not support PreparedStatement.getGeneratedKeys()."
							+ " Please use setUpdateAutoGeneratedKeys(false) in your Persist instance"
							+ " to disable attempts to use that feature");
				}
			}

			Result result = new Result(rowsModified, generatedKeys);

			if (Log.isDebugEnabled(Log.PROFILING)) {
				final long end = System.currentTimeMillis();
				Log.debug(Log.PROFILING, "executeUpdate in [" + (end - begin) + "ms] for sql [" + sql + "]");
			}

			return result;
		} finally {
			closePreparedStatement(stmt);
		}
	}

	/**
	 * Executes an update and returns the number of rows modified.
	 * <p>
	 * Parameters will be set according with the correspondence defined in
	 * {@link #setParameters(PreparedStatement, int[], Object[])}
	 * 
	 * @param sql SQL code to be executed.
	 * @param parameters Parameters to be used in the PreparedStatement.
	 * @since 1.0
	 */
	public int executeUpdate(final String sql, final Object...parameters) {

		final PreparedStatement stmt = getPreparedStatement(sql);
		int rowsModified = 0;

		try {
			setParameters(stmt, parameters);
			rowsModified = stmt.executeUpdate();
		} catch (SQLException e) {
			throw new RuntimeSQLException("Error executing sql [" + sql + "] with parameters "
					+ Arrays.toString(parameters) + ": " + e.getMessage(), e);
		} finally {
			closePreparedStatement(stmt);
		}

		return rowsModified;
	}

	// ---------- insert ----------

	/**
	 * Inserts an object into the database.
	 * 
	 * @since 1.0
	 */
	public int insert(final Object object) {
		final TableMapping mapping = getTableMapping(object.getClass(), "insert()");
		final String sql = mapping.getInsertSql();
		final String[] columns = mapping.getNotAutoGeneratedColumns();
		final Object[] parameters = getParametersFromObject(object, columns, mapping);

		int ret = 0;
		if (updateAutoGeneratedKeys) {
			if (mapping.supportsGetGeneratedKeys()) {
				final Result result = executeUpdate(object.getClass(), sql, mapping.getAutoGeneratedColumns(),
						parameters);
				setAutoGeneratedKeys(object, result);
				ret = result.getRowsModified();
			} else {
				throw new PersistException("While inserting instance of [" + object.getClass().getName()
						+ "] autoUpdateGeneratedKeys is set to [true] but the database doesn't support this feature");
			}
		} else {
			ret = executeUpdate(sql, parameters);
		}
		return ret;
	}

	/**
	 * Inserts a batch of objects into the database.
	 * 
	 * @since 1.0
	 */
	// TODO: use batch updates
	public int[] insertBatch(final Object...objects) {
		if (objects == null || objects.length == 0) {
			return new int[0];
		}
		final int[] results = new int[objects.length];
		for (int i = 0; i < objects.length; i++) {
			results[i] = insert(objects[i]);
		}
		return results;
	}

	// ---------- update ----------

	/**
	 * Updates an object in the database. The object will be identified using
	 * its mapped table's primary key. If no primary keys are defined in the
	 * mapped table, a {@link PersistException} will be thrown.
	 * 
	 * @since 1.0
	 */
	public int update(final Object object) {
		final TableMapping mapping = getTableMapping(object.getClass(), "update()");

		if (mapping.getPrimaryKeys().length == 0) {
			throw new PersistException("Table " + mapping.getTableName() + " doesn't have a primary key");
		}
		final String sql = mapping.getUpdateSql();
		final String[] columns = new String[mapping.getNotPrimaryKeys().length + mapping.getPrimaryKeys().length];
		int i = 0;
		for (String notPrimaryKey : mapping.getNotPrimaryKeys()) {
			columns[i++] = notPrimaryKey;
		}
		for (String primaryKey : mapping.getPrimaryKeys()) {
			columns[i++] = primaryKey;
		}
		final Object[] parameters = getParametersFromObject(object, columns, mapping);
		return executeUpdate(sql, parameters);
	}

	/**
	 * Updates a batch of objects in the database. The objects will be
	 * identified using their mapped table's primary keys. If no primary keys
	 * are defined in the mapped table, a {@link PersistException} will be
	 * thrown.
	 * 
	 * @since 1.0
	 */
	public int[] updateBatch(final Object...objects) {
		// TODO: use batch updates
		if (objects == null || objects.length == 0) {
			return new int[0];
		}
		int[] results = new int[objects.length];
		for (int i = 0; i < objects.length; i++) {
			results[i] = update(objects[i]);
		}
		return results;
	}

	// ---------- delete ----------

	/**
	 * Deletes an object in the database. The object will be identified using
	 * its mapped table's primary key. If no primary keys are defined in the
	 * mapped table, a PersistException will be thrown.
	 * 
	 * @since 1.0
	 */
	public int delete(final Object object) {
		final TableMapping mapping = getTableMapping(object.getClass(), "delete()");
		if (mapping.getPrimaryKeys().length == 0) {
			throw new PersistException("Table " + mapping.getTableName() + " doesn't have a primary key");
		}
		final String sql = mapping.getDeleteSql();
		final String[] columns = mapping.getPrimaryKeys();
		final Object[] parameters = getParametersFromObject(object, columns, mapping);
		return executeUpdate(sql, parameters);
	}

	/**
	 * Updates a batch of objects in the database. The objects will be
	 * identified using their matched table's primary keys. If no primary keys
	 * are defined in a given object, a PersistException will be thrown.
	 * 
	 * @since 1.0
	 */
	public int[] deleteBatch(final Object...objects) {
		// TODO: use batch updates
		if (objects == null || objects.length == 0) {
			return new int[0];
		}
		int[] results = new int[objects.length];
		for (int i = 0; i < objects.length; i++) {
			results[i] = delete(objects[i]);
		}
		return results;
	}

	// ---------- read ----------

	// --- single objects ---

	/**
	 * Reads a single object from the database by mapping the results of the SQL
	 * query into an instance of the given object class. Only the columns
	 * returned from the SQL query will be set into the object instance. If a
	 * given column can't be mapped to the target object instance, a
	 * {@link PersistException} will be thrown.
	 * 
	 * @since 1.0
	 */
	public <T> T read(final Class<T> objectClass, final String sql) {
		return read(objectClass, sql, (Object[]) null);
	}

	/**
	 * Reads a single object from the database by mapping the results of the
	 * parameterized SQL query into an instance of the given object class. Only
	 * the columns returned from the SQL query will be set into the object
	 * instance. If a given column can't be mapped to the target object
	 * instance, a {@link PersistException} will be thrown.
	 * <p>
	 * Parameters will be set according with the correspondence defined in
	 * {@link #setParameters(PreparedStatement, int[], Object[])}
	 * 
	 * @since 1.0
	 */
	public <T> T read(final Class<T> objectClass, final String sql, final Object...parameters) {
		final PreparedStatement stmt = getPreparedStatement(sql);
		return read(objectClass, stmt, parameters);
	}

	/**
	 * Reads a single object from the database by mapping the results of the
	 * execution of the given PreparedStatement into an instance of the given
	 * object class. Only the columns returned from the PreparedStatement
	 * execution will be set into the object instance. If a given column can't
	 * be mapped to the target object instance, a {@link PersistException}
	 * will be thrown.
	 * <p>
	 * Parameters will be set according with the correspondence defined in
	 * {@link #setParameters(PreparedStatement, int[], Object[])}
	 * 
	 * @since 1.0
	 */
	public <T> T read(final Class<T> objectClass, final PreparedStatement statement, final Object...parameters) {
		try {
			setParameters(statement, parameters);
			final ResultSet resultSet = statement.executeQuery();
			final T ret = read(objectClass, resultSet);
			return ret;
		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		} finally {
			if (closePreparedStatementsAfterRead) {
				closePreparedStatement(statement);
			}
		}
	}

	/**
	 * Reads a single object from the database by mapping the content of the
	 * ResultSet current row into an instance of the given object class. Only
	 * columns contained in the ResultSet will be set into the object instance.
	 * If a given column can't be mapped to the target object instance, a
	 * PersistException will be thrown.
	 * 
	 * @since 1.0
	 */
	public <T> T read(final Class<T> objectClass, final ResultSet resultSet) {
		long begin = 0;
		if (Log.isDebugEnabled(Log.PROFILING)) {
			begin = System.currentTimeMillis();
		}

		Object ret = null;
		try {
			if (resultSet.next()) {
				ret = loadObject(objectClass, resultSet);
				if (resultSet.next()) {
					throw new PersistException("Non-unique result returned");
				}
			}
		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		}

		if (Log.isDebugEnabled(Log.PROFILING)) {
			final long end = System.currentTimeMillis();
			Log.debug(Log.PROFILING, "read in [" + (end - begin) + "ms] for object type ["
					+ objectClass.getSimpleName() + "]");
		}

		return (T) ret;
	}

	/**
	 * Reads an object from the database by its primary keys.
	 * 
	 * @since 1.0
	 */
	public <T> T readByPrimaryKey(final Class<T> objectClass, final Object...primaryKeyValues) {
		final TableMapping mapping = getTableMapping(objectClass, "readByPrimaryKey()");
		final String sql = mapping.getSelectSql();
		return read(objectClass, sql, primaryKeyValues);
	}

	// --- lists ---

	/**
	 * Reads a list of objects from the database by mapping the content of the
	 * ResultSet into instances of the given object class. Only columns
	 * contained in the ResultSet will be set into the object instances. If a
	 * given column can't be mapped to a target object instance, a
	 * PersistException will be thrown.
	 * 
	 * @since 1.0
	 */
	public <T> List<T> readList(final Class<T> objectClass, final ResultSet resultSet) {

		long begin = 0;
		if (Log.isDebugEnabled(Log.PROFILING)) {
			begin = System.currentTimeMillis();
		}

		final List<T> ret = new ArrayList();
		try {
			while (resultSet.next()) {
				ret.add((T) loadObject(objectClass, resultSet));
			}
		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		}

		if (Log.isDebugEnabled(Log.PROFILING)) {
			final long end = System.currentTimeMillis();
			Log.debug(Log.PROFILING, "readList in [" + (end - begin) + "ms] for object type ["
					+ objectClass.getSimpleName() + "]");
		}

		return ret;
	}

	/**
	 * Reads a list of objects from the database by mapping the results of the
	 * execution of the given PreparedStatement into instances of the given
	 * object class. Only the columns returned from the PreparedStatement
	 * execution will be set into the object instances. If a given column can't
	 * be mapped to a target object instance, a PersistException will be
	 * thrown.
	 * <p>
	 * Parameters will be set according with the correspondence defined in
	 * {@link #setParameters(PreparedStatement, int[], Object[])}
	 * 
	 * @since 1.0
	 */
	public <T> List<T> readList(final Class<T> objectClass, final PreparedStatement statement,
			final Object...parameters) {
		setParameters(statement, parameters);
		try {
			final ResultSet resultSet = statement.executeQuery();
			return readList(objectClass, resultSet);
		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		}
	}

	/**
	 * Reads a list of objects from the database by mapping the results of the
	 * parameterized SQL query into instances of the given object class. Only
	 * the columns returned from the SQL query will be set into the object
	 * instance. If a given column can't be mapped to the target object
	 * instance, a {@link PersistException} will be thrown.
	 * <p>
	 * Parameters will be set according with the correspondence defined in
	 * {@link #setParameters(PreparedStatement, int[], Object[])}
	 * 
	 * @since 1.0
	 */
	public <T> List<T> readList(final Class<T> objectClass, final String sql, final Object...parameters) {
		final PreparedStatement stmt = getPreparedStatement(sql);
		try {
			return readList(objectClass, stmt, parameters);
		} finally {
			if (closePreparedStatementsAfterRead) {
				closePreparedStatement(stmt);
			}
		}
	}

	/**
	 * Reads a list of objects from the database by mapping the results of the
	 * SQL query into instances of the given object class. Only the columns
	 * returned from the SQL query will be set into the object instance. If a
	 * given column can't be mapped to the target object instance, a
	 * {@link PersistException} will be thrown.
	 * 
	 * @since 1.0
	 */
	public <T> List<T> readList(final Class<T> objectClass, final String sql) {
		return readList(objectClass, sql, (Object[]) null);
	}

	/**
	 * Reads a list of all objects in the database mapped to the given object
	 * class.
	 * 
	 * @since 1.0
	 */
	public <T> List<T> readList(final Class<T> objectClass) {
		final TableMapping mapping = getTableMapping(objectClass, "readList(Class)");
		final String sql = mapping.getSelectAllSql();
		return readList(objectClass, sql);
	}

	// --- iterators ---

	/**
	 * Returns an {@link java.util.Iterator} for a list of objects from the
	 * database that map the contents of the ResultSet into instances of the
	 * given object class. Only columns contained in the ResultSet will be set
	 * into the object instances. If a given column can't be mapped to a target
	 * object instance, a {@link PersistException} will be thrown.
	 * 
	 * @since 1.0
	 */
	public <T> Iterator<T> readIterator(final Class<T> objectClass, final ResultSet resultSet) {

		long begin = 0;
		if (Log.isDebugEnabled(Log.PROFILING)) {
			begin = System.currentTimeMillis();
		}

		final ResultSetIterator i = new ResultSetIterator(this, objectClass, resultSet, ResultSetIterator.TYPE_OBJECT);

		if (Log.isDebugEnabled(Log.PROFILING)) {
			final long end = System.currentTimeMillis();
			Log.debug(Log.PROFILING, "readIterator in [" + (end - begin) + "ms] for object type ["
					+ objectClass.getSimpleName() + "]");
		}

		return i;
	}

	/**
	 * Returns an {@link java.util.Iterator} for a list of objects from the
	 * database that map the contents of the execution of the given
	 * PreparedStatement into instances of the given object class. Only columns
	 * contained in the ResultSet will be set into the object instances. If a
	 * given column can't be mapped to a target object instance, a
	 * PersistException will be thrown.
	 * <p>
	 * Parameters will be set according with the correspondence defined in
	 * {@link #setParameters(PreparedStatement, int[], Object[])}
	 * 
	 * @since 1.0
	 */
	public <T> Iterator<T> readIterator(final Class<T> objectClass, final PreparedStatement statement,
			final Object...parameters) {
		setParameters(statement, parameters);
		try {
			final ResultSet resultSet = statement.executeQuery();
			return readIterator(objectClass, resultSet);
		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		}
	}

	/**
	 * Returns an {@link java.util.Iterator} for a list of objects from the
	 * database that map the contents of the execution of the given SQL query
	 * into instances of the given object class. Only columns contained in the
	 * ResultSet will be set into the object instances. If a given column can't
	 * be mapped to a target object instance, a {@link PersistException} will
	 * be thrown.
	 * <p>
	 * Parameters will be set according with the correspondence defined in
	 * {@link #setParameters(PreparedStatement, int[], Object[])}
	 * 
	 * @since 1.0
	 */
	public <T> Iterator<T> readIterator(final Class<T> objectClass, final String sql, final Object...parameters) {
		final PreparedStatement stmt = getPreparedStatement(sql);
		final Iterator ret = readIterator(objectClass, stmt, parameters);
		// don't close the prepared statement otherwise the result set in the
		// iterator will be closed
		return ret;
	}

	/**
	 * Returns an {@link java.util.Iterator} for a list of objects from the
	 * database that map the contents of the execution of the given SQL query
	 * into instances of the given object class. Only columns contained in the
	 * ResultSet will be set into the object instances. If a given column can't
	 * be mapped to a target object instance, a {@link PersistException} will
	 * be thrown.
	 * 
	 * @since 1.0
	 */
	public <T> Iterator<T> readIterator(final Class<T> objectClass, final String sql) {
		return readIterator(objectClass, sql, (Object[]) null);
	}

	/**
	 * Returns an {@link java.util.Iterator} for a list of all objects in the
	 * database mapped to the given object class.
	 * 
	 * @since 1.0
	 */
	public <T> Iterator<T> readIterator(final Class<T> objectClass) {
		final TableMapping mapping = getTableMapping(objectClass, "readIterator(Class)");
		final String sql = mapping.getSelectAllSql();
		return readIterator(objectClass, sql);
	}

	// ---------- read (map) ----------

	// --- single objects ---

	/**
	 * Reads a single object from the database by mapping the results of the SQL
	 * query into an instance of {@link java.util.Map}.
	 * <p>
	 * Types returned from the database will be converted to Java types in the
	 * map according with the correspondence defined in
	 * {@link #getValueFromResultSet(ResultSet, int, int)}.
	 * 
	 * @since 1.0
	 */
	public Map<String, Object> readMap(final String sql) {
		return readMap(sql, (Object[]) null);
	}

	/**
	 * Reads a single object from the database by mapping the results of the SQL
	 * query into an instance of {@link java.util.Map}.
	 * <p>
	 * Types returned from the database will be converted to Java types in the
	 * map according with the correspondence defined in
	 * {@link #getValueFromResultSet(ResultSet, int, int)}.
	 * <p>
	 * Parameters will be set according with the correspondence defined in
	 * {@link Persist#setParameters(PreparedStatement, int[], Object[])}
	 * 
	 * @since 1.0
	 */
	public Map<String, Object> readMap(final String sql, final Object...parameters) {
		final PreparedStatement stmt = getPreparedStatement(sql);
		try {
			return readMap(stmt, parameters);
		} finally {
			if (closePreparedStatementsAfterRead) {
				closePreparedStatement(stmt);
			}
		}
	}

	/**
	 * Reads a single object from the database by mapping the results of the
	 * PreparedStatement execution into an instance of {@link java.util.Map}.
	 * <p>
	 * Types returned from the database will be converted to Java types in the
	 * map according with the correspondence defined in
	 * {@link #getValueFromResultSet(ResultSet, int, int)}.
	 * <p>
	 * Parameters will be set according with the correspondence defined in
	 * {@link Persist#setParameters(PreparedStatement, int[], Object[])}
	 * 
	 * @since 1.0
	 */
	public Map<String, Object> readMap(final PreparedStatement statement, final Object...parameters) {
		setParameters(statement, parameters);
		try {
			final ResultSet resultSet = statement.executeQuery();
			return readMap(resultSet);
		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		}
	}

	/**
	 * Reads a single object from the database by mapping the results of the
	 * current ResultSet row into an instance of {@link java.util.Map}.
	 * <p>
	 * Types returned from the database will be converted to Java types in the
	 * map according with the correspondence defined in
	 * {@link #getValueFromResultSet(ResultSet, int, int)}.
	 * 
	 * @since 1.0
	 */
	public Map<String, Object> readMap(final ResultSet resultSet) {

		long begin = 0;
		if (Log.isDebugEnabled(Log.PROFILING)) {
			begin = System.currentTimeMillis();
		}

		Map<String, Object> ret = null;

		try {
			if (resultSet.next()) {
				ret = loadMap(resultSet);
				if (resultSet.next()) {
					throw new PersistException("Non-unique result returned");
				}
			} else {
				ret = null;
			}
		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		}

		if (Log.isDebugEnabled(Log.PROFILING)) {
			final long end = System.currentTimeMillis();
			Log.debug(Log.PROFILING, "readMap in [" + (end - begin) + "ms]");
		}

		return ret;
	}

	// --- list ---

	/**
	 * Reads a list of objects from the database by mapping the ResultSet rows
	 * to instances of {@link java.util.Map}.
	 * <p>
	 * Types returned from the database will be converted to Java types in the
	 * map according with the correspondence defined in
	 * {@link #getValueFromResultSet(ResultSet, int, int)}.
	 * 
	 * @since 1.0
	 */
	public List<Map<String, Object>> readMapList(final ResultSet resultSet) {

		long begin = 0;
		if (Log.isDebugEnabled(Log.PROFILING)) {
			begin = System.currentTimeMillis();
		}

		final List ret = new ArrayList();
		try {
			while (resultSet.next()) {
				ret.add(loadMap(resultSet));
			}
		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		}

		if (Log.isDebugEnabled(Log.PROFILING)) {
			final long end = System.currentTimeMillis();
			Log.debug(Log.PROFILING, "readMapList [" + (end - begin) + "ms]");
		}

		return ret;
	}

	/**
	 * Reads a list of objects from the database by mapping the
	 * PreparedStatement execution results to instances of {@link java.util.Map}.
	 * <p>
	 * Types returned from the database will be converted to Java types in the
	 * map according with the correspondence defined in
	 * {@link #getValueFromResultSet(ResultSet, int, int)}.
	 * <p>
	 * Parameters will be set according with the correspondence defined in
	 * {@link #setParameters(PreparedStatement, int[], Object[])}
	 * 
	 * @since 1.0
	 */
	public List<Map<String, Object>> readMapList(final PreparedStatement statement, final Object...parameters) {
		setParameters(statement, parameters);
		try {
			final ResultSet resultSet = statement.executeQuery();
			return readMapList(resultSet);
		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		}
	}

	/**
	 * Reads a list of objects from the database by mapping the SQL execution
	 * results to instances of {@link java.util.Map}.
	 * <p>
	 * Types returned from the database will be converted to Java types in the
	 * map according with the correspondence defined in
	 * {@link #getValueFromResultSet(ResultSet, int, int)}.
	 * <p>
	 * Parameters will be set according with the correspondence defined in
	 * {@link #setParameters(PreparedStatement, int[], Object[])}
	 * 
	 * @since 1.0
	 */
	public List<Map<String, Object>> readMapList(final String sql, final Object...parameters) {
		final PreparedStatement stmt = getPreparedStatement(sql);
		try {
		    return readMapList(stmt, parameters);
		} finally {
			if (closePreparedStatementsAfterRead) {
				closePreparedStatement(stmt);
			}
		}
	}

	/**
	 * Reads a list of all objects in the database mapped to the given object
	 * class and return each result as an instance of {@link java.util.Map}.
	 * <p>
	 * Types returned from the database will be converted to Java types in the
	 * map according with the correspondence defined in
	 * {@link #getValueFromResultSet(ResultSet, int, int)}.
	 * 
	 * @since 1.0
	 */
	public List<Map<String, Object>> readMapList(final String sql) {
		return readMapList(sql, (Object[]) null);
	}

	// --- iterator ---

	/**
	 * Returns an {@link java.util.Iterator} for a list of {@link java.util.Map}
	 * instances containing data from the provided ResultSet rows.
	 * <p>
	 * Types returned from the database will be converted to Java types in the
	 * map according with the correspondence defined in
	 * {@link #getValueFromResultSet(ResultSet, int, int)}.
	 * 
	 * @since 1.0
	 */
	public Iterator readMapIterator(final ResultSet resultSet) {
		return new ResultSetIterator(this, null, resultSet, ResultSetIterator.TYPE_MAP);
	}

	/**
	 * Returns an {@link java.util.Iterator} for a list of {@link java.util.Map}
	 * instances containing data from the execution of the provided
	 * PreparedStatement.
	 * <p>
	 * Types returned from the database will be converted to Java types in the
	 * map according with the correspondence defined in
	 * {@link #getValueFromResultSet(ResultSet, int, int)}.
	 * <p>
	 * Parameters will be set according with the correspondence defined in
	 * {@link Persist#setParameters(PreparedStatement, int[], Object[])}
	 * 
	 * @since 1.0
	 */
	public Iterator readMapIterator(final PreparedStatement statement, final Object...parameters) {
		setParameters(statement, parameters);
		try {
			final ResultSet resultSet = statement.executeQuery();
			return readMapIterator(resultSet);
		} catch (SQLException e) {
			throw new RuntimeSQLException(e);
		}
	}

	/**
	 * Returns an {@link java.util.Iterator} for a list of {@link java.util.Map}
	 * instances containing data from the execution of the provided parametrized
	 * SQL.
	 * <p>
	 * Types returned from the database will be converted to Java types in the
	 * map according with the correspondence defined in
	 * {@link #getValueFromResultSet(ResultSet, int, int)}.
	 * <p>
	 * Parameters will be set according with the correspondence defined in
	 * {@link #setParameters(PreparedStatement, int[], Object[])}
	 * 
	 * @since 1.0
	 */
	public Iterator readMapIterator(final String sql, final Object...parameters) {

		long begin = 0;
		if (Log.isDebugEnabled(Log.PROFILING)) {
			begin = System.currentTimeMillis();
		}

		final PreparedStatement stmt = getPreparedStatement(sql);
		final Iterator ret = readMapIterator(stmt, parameters);

		if (Log.isDebugEnabled(Log.PROFILING)) {
			final long end = System.currentTimeMillis();
			Log.debug(Log.PROFILING, "readMapIterator in [" + (end - begin) + "ms]");
		}

		return ret;
	}

	/**
	 * Returns an {@link java.util.Iterator} for a list of {@link java.util.Map}
	 * instances containing data from the execution of the provided SQL.
	 * <p>
	 * Types returned from the database will be converted to Java types in the
	 * map according with the correspondence defined in
	 * {@link #getValueFromResultSet(ResultSet, int, int)}.
	 * 
	 * @since 1.0
	 */
	public Iterator readMapIterator(final String sql) {
		return readMapIterator(sql, (Object[]) null);
	}

}
